﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
using System.Data.Common;
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using ICSharpCode.SharpZipLib.Checksums;
using System.Xml;
using System.Globalization;

/// <summary>
/// Generates resources and outputs to various formats
/// </summary>
public static class ResourceGenerator
{
    /// <summary>
    /// Creates Dictionaries to store bundles from the database.
    /// </summary>
    /// <param name="applicationName">Name of the application.</param>
    /// <param name="applicationId">The application ID.</param>
    /// <param name="threshold">The completion threshold.</param>
    /// <param name="includedBundles">The bundles to include.</param>
    /// <param name="includedLanguages">The language codes to include.</param>
    /// <param name="defaultLanguage">The default language.</param>
    /// <param name="includeSummary"><c>True</c> if a summary of the generated resources should be included.</param>
    /// <returns>A KeyValuePair containing <summary (or null if not needed), Dictionary<bundlename, Dictionary<language-code, Dictionary<element, translation>>>></returns>
    public static KeyValuePair<string, Dictionary<string, Dictionary<string, Dictionary<string, string>>>> GenerateBundles(string applicationName, string applicationId, int threshold, string[] includedBundles, string[] includedLanguages, string defaultLanguage, bool includeSummary) {
        Dictionary<string, Dictionary<string, Dictionary<string, string>>> bundles = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>(); // <BundleName, <abbrev, <elementname, translation>>>
        List<string> errors = new List<string>();
        StringBuilder summary = null;
        if(includeSummary) {
            summary = new StringBuilder().AppendFormat("Resources generated by ResourceBlender v{0} on {1}\n\n",Constants.Version.ToString(3),DateTime.Now.ToShortDateString());
        }

        Dictionary<string, int> completionMap = new Dictionary<string, int>();
        int totalStrings = 0;
        Dictionary<string, Dictionary<string, string>> defaultLanguageStrings = new Dictionary<string, Dictionary<string, string>>();
        Database db = new Database();
        db.OpenConnection();
        using(Database.Command cmd = db.CreateCommand()) {
            cmd.CommandText = "SELECT language, bundle, elementname, translation, bundles.name AS bundlename FROM elements INNER JOIN translations ON translations.translationid = elements.translationid LEFT JOIN bundles on elements.bundle = bundles.id WHERE applicationid = @applicationId AND language = @defaultLang" + db.GenerateInClause("bundle", includedBundles, true);
            cmd.AddParameterWithValue("@applicationId", applicationId);
            cmd.AddParameterWithValue("@defaultLang", defaultLanguage);
            DbDataReader reader = cmd.ExecuteReader();
            while(reader.Read()) { // generate a dictionary for each language, fill it with entries we have
                string bundleName = reader["bundlename"].ToString(); // id of the bundle
                string elementName = reader["elementname"].ToString(); // eg: element
                if(!defaultLanguageStrings.ContainsKey(bundleName)) { //  first element in this form name
                    defaultLanguageStrings[bundleName] = new Dictionary<string, string>();
                }
                defaultLanguageStrings[bundleName][elementName] = reader["translation"].ToString();
                totalStrings++;
            }
            reader.Close();
            if(includeSummary) {
                summary.AppendFormat("Application '{0}' contains {1} bundle{2}:\n", applicationName, defaultLanguageStrings.Count, defaultLanguageStrings.Count == 1 ? "" : "s");
                foreach(KeyValuePair<string, Dictionary<string, string>> bundle in defaultLanguageStrings) {
                    summary.AppendFormat("*{0} ({1} element{2})\n", bundle.Key, bundle.Value.Count, bundle.Value.Count == 1 ? "" : "s");
                }
                summary.AppendFormat("\nOmitting languages less than {0}% complete\n", threshold);
            }

            cmd.CommandText = "SELECT language, bundle, elementname, translation, bundles.name AS bundlename FROM elements INNER JOIN translations ON translations.translationid = elements.translationid LEFT JOIN bundles on elements.bundle = bundles.id WHERE applicationid = @applicationId AND language != @defaultLang" + db.GenerateInClause("language", includedLanguages, true) + db.GenerateInClause("bundle", includedBundles, true);
            reader = cmd.ExecuteReader();
            while(reader.Read()) { // generate a dictionary for each language, fill it with entries we have
                string abbrev = reader["language"].ToString();
                string bundle = reader["bundle"].ToString(); // id of the bundle
                string bundleName = reader["bundlename"].ToString(); // eg: bundle1
                string elementName = reader["elementname"].ToString(); // eg: element
                if(includedBundles.Contains(bundle)) {
                    if(!completionMap.ContainsKey(abbrev)) {
                        completionMap[abbrev] = 0;
                    }
                    completionMap[abbrev]++;
                    if(!bundles.ContainsKey(bundleName)) { //  first element in this bundle
                        bundles[bundleName] = new Dictionary<string, Dictionary<string, string>>();
                    }
                    if(!bundles[bundleName].ContainsKey(abbrev)) {
                        bundles[bundleName][abbrev] = new Dictionary<string, string>();
                    }
                    bundles[bundleName][abbrev][elementName] = reader["translation"].ToString();
                }
            }
            reader.Close();

            if(includedLanguages.Contains(defaultLanguage)) {
                if(includeSummary) {
                    summary.AppendFormat("*Included {0} ({1}), 100% complete\n", CultureInfo.GetCultureInfo(defaultLanguage).EnglishName, defaultLanguage);
                }
                foreach(KeyValuePair<string, Dictionary<string, string>> defaultBundle in defaultLanguageStrings) { // the default language is loaded separately from the rest, merge them here
                    bundles[defaultBundle.Key][defaultLanguage] = new Dictionary<string, string>();
                    foreach(KeyValuePair<string, string> element in defaultBundle.Value) {
                        bundles[defaultBundle.Key][defaultLanguage][element.Key] = element.Value;
                    }
                }
            }

            // remove languages below the completion threshold
            List<string> incompleteLanguages = new List<string>();
            foreach(KeyValuePair<string, int> languageCompletion in completionMap) { // key=abbrev, val=number of translatedStrings
                int completionPercentage = (int)Math.Round((languageCompletion.Value / (double)totalStrings) * 100);
                if(completionPercentage > threshold) {
                    if(includeSummary) {
                        summary.AppendFormat("*Included {0} ({1}), {2}% complete\n", CultureInfo.GetCultureInfo(languageCompletion.Key).EnglishName, languageCompletion.Key, completionPercentage);
                    }
                    foreach(KeyValuePair<string, Dictionary<string, string>> bundle in defaultLanguageStrings) { // key=bundlename, val=Dictionary<element,translation>
                        if(!bundles.ContainsKey(bundle.Key)) {
                            bundles[bundle.Key] = new Dictionary<string, Dictionary<string, string>>();
                        }
                        if(!bundles[bundle.Key].ContainsKey(languageCompletion.Key)) {
                            bundles[bundle.Key].Add(languageCompletion.Key, bundle.Value);
                        } else {
                            foreach(KeyValuePair<string, string> element in bundle.Value) {
                                if(!bundles[bundle.Key][languageCompletion.Key].ContainsKey(element.Key)) {
                                    bundles[bundle.Key][languageCompletion.Key].Add(element.Key, element.Value);
                                }
                            }
                        }
                    }
                } else {
                    if(includeSummary) {
                        summary.AppendFormat("*Omitted {0} ({1}), {2}% complete\n", CultureInfo.GetCultureInfo(languageCompletion.Key).EnglishName, languageCompletion.Key, completionPercentage);
                    }
                    foreach(KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> form in bundles) {
                        form.Value.Remove(languageCompletion.Key);
                    }
                }
            }
            return new KeyValuePair<string, Dictionary<string, Dictionary<string, Dictionary<string, string>>>>(includeSummary ? summary.ToString() : null, bundles);
        }
    }

    /// <summary>
    /// Outputs to Firefox .properties and .dtd files.
    /// </summary>
    /// <param name="applicationName">Name of the application.</param>
    /// <param name="applicationId">The application ID.</param>
    /// <param name="threshold">The completion threshold.</param>
    /// <param name="includedBundles">The bundles to include.</param>
    /// <param name="languages">The language codes to include.</param>
    /// <param name="defaultLanguage">The default language.</param>
    /// <param name="includeSummary"><c>true</c> if a summary of generated resources should be included in the zip, otherwise <c>false</c>.</param>
    public static void GenerateFirefox(string applicationName, string applicationId, int threshold, string[] includedBundles, string[] includedLanguages, string defaultLanguage, bool includeSummary) {
        Dictionary<string, Dictionary<string, Dictionary<string, string>>> translations = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>(); // <BundleName, <abbrev, <elementname, translation>>>
        List<string> errors = new List<string>();
        StringBuilder summary = null;
        if(includeSummary) {
            summary = new StringBuilder("Resources generated by ResourceBlender v").Append(Constants.Version.ToString(3))
                                .Append(" on ").Append(DateTime.Now.ToShortDateString()).Append("\n");
        }

        Dictionary<string, int> completionMap = new Dictionary<string, int>();
        int totalStrings = 0;
        Dictionary<string, Dictionary<string, string>> defaultLanguageStrings = new Dictionary<string, Dictionary<string, string>>();
        Database db = new Database();
        db.OpenConnection();
        using(Database.Command cmd = db.CreateCommand()) {
            cmd.CommandText = "SELECT language, bundle, elementname, translation, bundles.name AS bundlename FROM elements INNER JOIN translations ON translations.translationid = elements.translationid LEFT JOIN bundles on elements.bundle = bundles.id WHERE applicationid = @applicationId AND language = @defaultLang" + db.GenerateInClause("bundle", includedBundles, true);
            cmd.AddParameterWithValue("@applicationId", applicationId);
            cmd.AddParameterWithValue("@defaultLang", defaultLanguage);
            DbDataReader reader = cmd.ExecuteReader();
            while(reader.Read()) { // generate a dictionary for each language, fill it with entries we have
                string bundleName = reader["bundlename"].ToString(); // JavaScript / XUL
                string elementName = reader["elementname"].ToString(); // eg: element
                if(!defaultLanguageStrings.ContainsKey(bundleName)) { //  first element in this form name
                    defaultLanguageStrings[bundleName] = new Dictionary<string, string>();
                }
                defaultLanguageStrings[bundleName][elementName] = reader["translation"].ToString();
                totalStrings++;
            }
            reader.Close();
            if(includeSummary) {
                summary.AppendFormat("Firefox extension '{0}' contains {1} bundle{2}:\n", applicationName, defaultLanguageStrings.Count, defaultLanguageStrings.Count == 1 ? "" : "s");
                foreach(KeyValuePair<string, Dictionary<string, string>> bundle in defaultLanguageStrings) {
                    summary.AppendFormat("*{0} ({1} element{2})\n", bundle.Key, bundle.Value.Count, bundle.Value.Count == 1 ? "" : "s");
                }
                summary.AppendFormat("\nOmitting languages less than {0}% complete\n", threshold);
            }

            cmd.CommandText = "SELECT language, bundle, elementname, translation, bundles.name AS bundlename FROM elements INNER JOIN translations ON translations.translationid = elements.translationid LEFT JOIN bundles on elements.bundle = bundles.id WHERE applicationid = @applicationId AND language != @defaultLang" + db.GenerateInClause("language", includedLanguages, true) + db.GenerateInClause("bundle", includedBundles, true);
            reader = cmd.ExecuteReader();
            while(reader.Read()) { // generate a dictionary for each language, fill it with entries we have
                string abbrev = reader["language"].ToString();
                string bundle = reader["bundle"].ToString();
                string bundleName = reader["bundlename"].ToString(); // JavaScript / XUL
                string elementName = reader["elementname"].ToString(); // eg: element
                if(includedBundles.Contains(bundle) && includedLanguages.Contains(abbrev)) {
                    if(!completionMap.ContainsKey(abbrev)) {
                        completionMap[abbrev] = 0;
                    }
                    completionMap[abbrev]++;
                    if(!translations.ContainsKey(abbrev)) { //  first element in this form name
                        translations[abbrev] = new Dictionary<string, Dictionary<string, string>>();
                    }
                    if(!translations[abbrev].ContainsKey(bundleName)) {
                        translations[abbrev][bundleName] = new Dictionary<string, string>();
                    }
                    translations[abbrev][bundleName][elementName] = reader["translation"].ToString();
                }
            }
            reader.Close();

            if(includedLanguages.Contains(defaultLanguage)) {
                if(includeSummary) {
                    summary.AppendFormat("*Included {0} ({1}), 100% complete\n", CultureInfo.GetCultureInfo(defaultLanguage).EnglishName, defaultLanguage);
                }
                translations[defaultLanguage] = new Dictionary<string, Dictionary<string, string>>();
                foreach(KeyValuePair<string, Dictionary<string, string>> defaultBundle in defaultLanguageStrings) {
                    translations[defaultLanguage][defaultBundle.Key] = new Dictionary<string, string>();
                    foreach(KeyValuePair<string, string> element in defaultBundle.Value) {
                        translations[defaultLanguage][defaultBundle.Key][element.Key] = element.Value;
                    }
                }
            }

            // remove languages below the completion threshold
            List<string> incompleteLanguages = new List<string>();
            foreach(KeyValuePair<string, int> languageCompletion in completionMap) { // key=abbrev, val=translatedStrings
                int completionPercentage = (int)Math.Round((languageCompletion.Value / (double)totalStrings) * 100);
                if(completionPercentage > threshold) {
                    if(includeSummary) {
                        summary.AppendFormat("*Included {0} ({1}), {2}% complete\n", CultureInfo.GetCultureInfo(languageCompletion.Key).EnglishName, languageCompletion.Key, completionPercentage);
                    }
                    if(!translations.ContainsKey(languageCompletion.Key)) { // there are no translations for this language, use the default language instead
                        translations[languageCompletion.Key] = defaultLanguageStrings;
                    } else {
                        foreach(KeyValuePair<string, Dictionary<string, string>> bundle in defaultLanguageStrings) {
                            if(!translations[languageCompletion.Key].ContainsKey(bundle.Key)) { // some translations exist, but this bundle does not, use the default language for this bundle
                                translations[languageCompletion.Key][bundle.Key] = bundle.Value;
                            } else {
                                foreach(KeyValuePair<string, string> element in bundle.Value) {
                                    if(!translations[languageCompletion.Key][bundle.Key].ContainsKey(element.Key)) {
                                        translations[languageCompletion.Key][bundle.Key].Add(element.Key, element.Value);
                                    }
                                }
                            }
                        }
                    }
                } else {
                    if(includeSummary) {
                        summary.AppendFormat("*Omitted {0} ({1}), {2}% complete\n", CultureInfo.GetCultureInfo(languageCompletion.Key).EnglishName, languageCompletion.Key, completionPercentage);
                    }
                    translations.Remove(languageCompletion.Key);
                }
            }
        }

        MemoryStream ms = new MemoryStream();
        ZipOutputStream zipStream = new ZipOutputStream(ms);
        zipStream.SetLevel(9);
        Crc32 crc = new Crc32();
        StringBuilder translationBuffer;

        foreach(KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> language in translations) {
            /* language.Key = language abbreviation
               language.Value = Dictionary<bundleName, Dictionary<element, translation>> */
            ZipEntry zipEntry = new ZipEntry(language.Key + "\\contents.rdf");
            string cultureName = string.Empty;
            try {
                cultureName = System.Globalization.CultureInfo.GetCultureInfo(language.Key).EnglishName;
            } catch { }
            translationBuffer = new StringBuilder();
            translationBuffer.Append(string.Format(Constants.ContentsRDF, language.Key, cultureName, applicationName));
            zipEntry.DateTime = DateTime.Now;
            byte[] buffer = Encoding.UTF8.GetBytes(translationBuffer.ToString());
            zipEntry.Size = buffer.Length;

            zipStream.PutNextEntry(zipEntry);
            zipStream.Write(buffer, 0, buffer.Length);

            foreach(KeyValuePair<string, Dictionary<string, string>> bundle in language.Value) {
                /* bundle.Key = bundleName
                   bundle.Value = Dictionary<element, translation> */
                translationBuffer = new StringBuilder();
                switch(bundle.Key.ToLower()) {
                    case "javascript":
                        zipEntry = new ZipEntry(language.Key + "\\" + applicationName + ".properties");
                        foreach(KeyValuePair<string, string> jsElement in bundle.Value) {
                            translationBuffer.Append(jsElement.Key).Append("=").Append(jsElement.Value).Append("\n");
                        }
                        break;
                    case "xul":
                        zipEntry = new ZipEntry(language.Key + "\\" + applicationName + ".dtd");
                        foreach(KeyValuePair<string, string> xulElement in bundle.Value) {
                            translationBuffer.Append("<!ENTITY ").Append(xulElement.Key).Append(" \"").Append(xulElement.Value).Append("\">\n");
                        }
                        break;
                }

                zipEntry.DateTime = DateTime.Now;
                buffer = Encoding.UTF8.GetBytes(translationBuffer.ToString());
                zipEntry.Size = buffer.Length;
                zipStream.PutNextEntry(zipEntry);
                zipStream.Write(buffer, 0, buffer.Length);
            }
        }

        if(includeSummary) {
            ZipEntry zipEntry = new ZipEntry("summary.txt");
            zipEntry.DateTime = DateTime.Now;
            byte[] summaryBytes = Encoding.UTF8.GetBytes(summary.ToString());
            zipEntry.Size = summaryBytes.Length;
            zipStream.PutNextEntry(zipEntry);
            zipStream.Write(summaryBytes, 0, summaryBytes.Length);
        }

        SendZip(ms, zipStream, applicationName);
    }

    /// <summary>
    /// Outputs to .NET resx files.
    /// </summary>
    /// <param name="applicationName">Name of the application.</param>
    /// <param name="applicationId">The application ID.</param>
    /// <param name="threshold">The completion threshold.</param>
    /// <param name="bundles">The bundles to include.</param>
    /// <param name="languages">The language codes to include.</param>
    /// <param name="defaultLanguage">The default language.</param>
    /// <param name="includeSummary"><c>true</c> if a summary of generated resources should be included in the zip, otherwise <c>false</c>.</param>
    /// <param name="compactFramework"><c>true</c> if the target platform is the Compact Framework, otherwise <c>false</c>.</param>
    public static void GenerateDotNet(string applicationName, string applicationId, int threshold, string[] includedBundles, string[] includedLanguages, string defaultLanguage, bool includeSummary, bool compactFramework) {
        KeyValuePair<string, Dictionary<string, Dictionary<string, Dictionary<string, string>>>> rawResources = GenerateBundles(applicationName, applicationId, threshold, includedBundles, includedLanguages, defaultLanguage, includeSummary);
        Dictionary<string, Dictionary<string, Dictionary<string, string>>> forms = rawResources.Value;

        MemoryStream ms = new MemoryStream();
        ZipOutputStream zipStream = new ZipOutputStream(ms);
        zipStream.SetLevel(9);
        Crc32 crc = new Crc32();

        foreach(KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> form in forms) {
            /* form.Key = bundlename
               form.Value = Dictionary<abbrev, Dictionary<location, translation>> */
            foreach(KeyValuePair<string, Dictionary<string, string>> lang in form.Value) {
                /* lang.Key = abbrev
                   lang.Value = Dictionary<location, translation> */
                XmlDocument resx = new XmlDocument();
                resx.LoadXml(Constants.ResxSkeleton);
                XmlNode root = resx.SelectSingleNode("/root");

                string langAbbreviation = string.Empty;
                if(compactFramework) {
                    switch(lang.Key) {
                        case "zh-Hans": // buggy .NET Framework and contradictory MSDN articles
                            langAbbreviation = "zh-CHS";
                            break;
                        case "zh-Hant": // buggy .NET Framework and contradictory MSDN articles
                            langAbbreviation = "zh-CHT";
                            break;
                        default:
                            langAbbreviation = lang.Key;
                            break;
                    }
                } else {
                    langAbbreviation = lang.Key;
                }
                List<string> addedElements = new List<string>();
                String templateFilePath = HttpContext.Current.Server.MapPath(Path.Combine(Path.Combine("templates", applicationName), form.Key + "." + langAbbreviation + ".resx")); // try specific language first
                if(!File.Exists(templateFilePath)) { // just use the default
                    templateFilePath = HttpContext.Current.Server.MapPath(Path.Combine(Path.Combine("templates", applicationName), form.Key + ".resx"));
                }
                if(File.Exists(templateFilePath)) {
                    XmlDocument templateFile = new XmlDocument();
                    templateFile.Load(templateFilePath);
                    XmlNodeList existingValues = templateFile.SelectNodes("/root/data");
                    foreach(XmlNode node in existingValues) {
                        XmlNode newNode = resx.ImportNode(node, true);
                        root.AppendChild(newNode);
                        //TODO: check if the attribute doesn't exist - is exception thrown?
                        addedElements.Add(node.Attributes.GetNamedItem("name").InnerText);
                    }
                }
                foreach(KeyValuePair<string, string> translatedString in lang.Value) {
                    if(!addedElements.Contains(translatedString.Key)) {
                        XmlNode nodeTranslationData = resx.CreateElement("data");
                        nodeTranslationData.Attributes.SetNamedItem(resx.CreateAttribute("name")).InnerText = translatedString.Key;
                        nodeTranslationData.AppendChild(resx.CreateElement("value")).InnerText = translatedString.Value;
                        root.AppendChild(nodeTranslationData);
                    }
                }

                ZipEntry zipEntry;
                using(MemoryStream msEntry = new MemoryStream()) {
                    zipEntry = new ZipEntry(form.Key + "." + langAbbreviation + ".resx");
                    zipEntry.DateTime = DateTime.Now;
                    resx.Save(msEntry);
                    byte[] buffer = msEntry.ToArray();
                    msEntry.Close();
                    msEntry.Dispose();
                    zipEntry.Size = buffer.Length;

                    zipStream.PutNextEntry(zipEntry);
                    zipStream.Write(buffer, 0, buffer.Length);

                    if(form.Key == "General" && lang.Key == defaultLanguage) { // todo: move to meta field in DB? universal fields?
                        zipEntry = new ZipEntry(form.Key + ".resx");
                        zipEntry.DateTime = DateTime.Now;
                        zipEntry.Size = buffer.Length;
                        zipStream.PutNextEntry(zipEntry);
                        zipStream.Write(buffer, 0, buffer.Length);
                    }
                }
            }
        }

        if(includeSummary) {
            ZipEntry zipEntry = new ZipEntry("summary.txt");
            zipEntry.DateTime = DateTime.Now;
            byte[] summaryBytes = Encoding.UTF8.GetBytes(rawResources.Key.ToString());
            zipEntry.Size = summaryBytes.Length;
            zipStream.PutNextEntry(zipEntry);
            zipStream.Write(summaryBytes, 0, summaryBytes.Length);
        }

        SendZip(ms, zipStream, applicationName);
    }


    /// <summary>
    /// Outputs bundles to Java .properties files. This could be done using a template, but Java uses subtly different language codes
    /// </summary>
    /// <param name="applicationName">Name of the application.</param>
    /// <param name="applicationId">The application ID.</param>
    /// <param name="threshold">The completion threshold.</param>
    /// <param name="includedBundles">The included bundles.</param>
    /// <param name="languages">The language codes to include.</param>
    /// <param name="defaultLanguage">The default language.</param>
    /// <param name="includeSummary"><c>true</c> if a summary of generated resources should be included in the zip, otherwise <c>false</c>.</param>
    public static void GenerateJava(string applicationName, string applicationId, int threshold, string[] includedBundles, string[] includedLanguages, string defaultLanguage, bool includeSummary) {
        KeyValuePair<string, Dictionary<string, Dictionary<string, Dictionary<string, string>>>> rawResources = GenerateBundles(applicationName, applicationId, threshold, includedBundles, includedLanguages, defaultLanguage, includeSummary);
        MemoryStream ms = new MemoryStream();
        ZipOutputStream zipStream = new ZipOutputStream(ms);
        zipStream.SetLevel(9);
        Crc32 crc = new Crc32();
        StringBuilder translationBuffer;
        ZipEntry zipEntry;
        byte[] buffer;

        Dictionary<string, Dictionary<string, Dictionary<string, string>>> bundles = rawResources.Value;
        foreach(KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> bundle in bundles) {
            /* bundle.Key = bundlename
               bundle.Value = Dictionary<abbrev, Dictionary<element, translation>> */
            foreach(KeyValuePair<string, Dictionary<string, string>> lang in bundle.Value) {
                /* lang.Key = abbrev
                   lang.Value = Dictionary<element, translation> */
                translationBuffer = new StringBuilder();
                string langAbbreviation = Utils.DotNetCulture2JavaCulture(lang.Key);

                zipEntry = new ZipEntry(bundle.Key + "_" + langAbbreviation + ".properties");
                zipEntry.DateTime = DateTime.Now;
                foreach(KeyValuePair<string, string> translation in lang.Value) {
                    translationBuffer.Append(translation.Key).Append("=").AppendLine(translation.Value);
                }
                buffer = Encoding.UTF8.GetBytes(translationBuffer.ToString());
                zipEntry.Size = buffer.Length;
                zipStream.PutNextEntry(zipEntry);
                zipStream.Write(buffer, 0, buffer.Length);
            }
        }

        if(rawResources.Key != null) {
            zipEntry = new ZipEntry("summary.txt");
            zipEntry.DateTime = DateTime.Now;
            byte[] summaryBytes = Encoding.UTF8.GetBytes(rawResources.Key);
            zipEntry.Size = summaryBytes.Length;
            zipStream.PutNextEntry(zipEntry);
            zipStream.Write(summaryBytes, 0, summaryBytes.Length);
        }

        SendZip(ms, zipStream, applicationName);
    }


    /// <summary>
    /// Generates resources using a custom template.
    /// </summary>
    /// <param name="applicationName">Name of the application.</param>
    /// <param name="applicationId">The application ID.</param>
    /// <param name="rawResources">The raw resources.</param>
    /// <param name="fileName">Name of each file.</param>
    /// <param name="fileStart">The text to prepend to the start of each file.</param>
    /// <param name="fileEnd">The text to append to the end of each file.</param>
    /// <param name="lineFormat">The line format.</param>
    public static void GenerateCustom(string applicationName, KeyValuePair<string, Dictionary<string, Dictionary<string, Dictionary<string, string>>>> rawResources, string fileName, string fileStart, string fileEnd, string lineFormat) {
        MemoryStream ms = new MemoryStream();
        ZipOutputStream zipStream = new ZipOutputStream(ms);
        zipStream.SetLevel(9);
        Crc32 crc = new Crc32();
        StringBuilder translationBuffer;
        ZipEntry zipEntry;
        byte[] buffer;

        string[] bundleTags = new string[] { "{=LanguageCode}",
                                             "{=Bundle}" }, // valid only for the current bundle (filename, filestart, fileend)
                 elementTags = new string[] { "{=LanguageCode}",
                                              "{=Bundle}",
                                              "{=Element}",
                                              "{=Translation}"
                                               }, // valid for each element
                 replacementBundleTags = new string[] { "", "" },
                 replacementElementTags = new string[] { "", "", "", "" };
        Dictionary<string, Dictionary<string, Dictionary<string, string>>> bundles = rawResources.Value;
        foreach(KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> bundle in bundles) {
            /* bundle.Key = bundlename
               bundle.Value = Dictionary<abbrev, Dictionary<element, translation>> */
            replacementBundleTags[1] = bundle.Key;
            replacementElementTags[1] = bundle.Key;
            foreach(KeyValuePair<string, Dictionary<string, string>> lang in bundle.Value) {
                /* lang.Key = abbrev
                   lang.Value = Dictionary<element, translation> */
                replacementBundleTags[0] = lang.Key;
                replacementElementTags[0] = lang.Key;
                translationBuffer = new StringBuilder();
                if(fileStart.Length > 0) {
                    translationBuffer.AppendLine(Utils.FormatTemplateTags(fileStart, bundleTags, replacementBundleTags));
                }
                zipEntry = new ZipEntry(Utils.FormatTemplateTags(fileName, bundleTags, replacementBundleTags));
                zipEntry.DateTime = DateTime.Now;
                foreach(KeyValuePair<string, string> translation in lang.Value) {
                    replacementElementTags[2] = translation.Key;
                    replacementElementTags[3] = translation.Value;
                    translationBuffer.AppendLine(Utils.FormatTemplateTags(lineFormat, elementTags, replacementElementTags));
                }
                if(fileEnd.Length > 0) {
                    translationBuffer.AppendLine(Utils.FormatTemplateTags(fileEnd, bundleTags, replacementBundleTags));
                }
                buffer = Encoding.UTF8.GetBytes(translationBuffer.ToString());
                zipEntry.Size = buffer.Length;
                zipStream.PutNextEntry(zipEntry);
                zipStream.Write(buffer, 0, buffer.Length);
            }
        }

        string summary = rawResources.Key;
        if(summary != null) {
            zipEntry = new ZipEntry("summary.txt");
            zipEntry.DateTime = DateTime.Now;
            byte[] summaryBytes = Encoding.UTF8.GetBytes(summary);
            zipEntry.Size = summaryBytes.Length;
            zipStream.PutNextEntry(zipEntry);
            zipStream.Write(summaryBytes, 0, summaryBytes.Length);
        }

        SendZip(ms, zipStream, applicationName);
    }

    /// <summary>
    /// Sends a zip file to the browse.
    /// </summary>
    /// <param name="zipStream">The zip stream.</param>
    /// <param name="fileName">Name of the file.</param>
    public static void SendZip(MemoryStream ms, ZipOutputStream zipStream, string fileName) {
        zipStream.Finish();
        HttpResponse res = HttpContext.Current.Response;
        res.Clear();
        res.AddHeader("Content-Type", "binary/octet-stream");
        res.AddHeader("Content-Length", zipStream.Length.ToString());
        res.AddHeader("Content-Disposition", "attachment; filename=\"" + fileName + ".zip\"; size=" + zipStream.Length.ToString()); // todo: include date in filename
        res.Flush();

        res.BinaryWrite(ms.ToArray());
        res.Flush();
        res.End();

        ms.Close();
        ms.Dispose();
    }

    
}
